Key Takeaway: Countries with higher GDP tend to have more positive migration rates, while those facing economic hardship or instability show higher emigration.

Understanding Global Migration Patterns

Where Are People Moving? ✈

Migration is a defining issue of our time, and its patterns vary widely across the world. Through this report, we explore how people are moving, how trends have changed over time, and how migration relates to economic and social indicators like GDP and life expectancy.

UNICEF data is used to tell this story through visualisations created in Python.

Code
import pandas as pd

# Load the datasets
migration = pd.read_csv('/content/unicef_indicator_2.csv')
metadata = pd.read_csv('/content/unicef_metadata.csv')

# Preview
migration.head()
country alpha_2_code alpha_3_code numeric_code indicator time_period obs_value sex unit_multiplier unit_of_measure observation_status observation_confidentaility time_period_activity_related_to_when_the_data_are_collected current_age
0 Afghanistan AF AFG 4 Net migration rate (per 1,000 population) 1950 0.792 Total NaN Number Predicted NaN NaN Total
1 Afghanistan AF AFG 4 Net migration rate (per 1,000 population) 1951 0.622 Total NaN Number Predicted NaN NaN Total
2 Afghanistan AF AFG 4 Net migration rate (per 1,000 population) 1952 0.018 Total NaN Number Predicted NaN NaN Total
3 Afghanistan AF AFG 4 Net migration rate (per 1,000 population) 1953 -1.095 Total NaN Number Predicted NaN NaN Total
4 Afghanistan AF AFG 4 Net migration rate (per 1,000 population) 1954 -0.833 Total NaN Number Predicted NaN NaN Total
Code
!pip install plotnine geopandas
Requirement already satisfied: plotnine in /usr/local/lib/python3.11/dist-packages (0.14.5)
Requirement already satisfied: geopandas in /usr/local/lib/python3.11/dist-packages (1.0.1)
Requirement already satisfied: matplotlib>=3.8.0 in /usr/local/lib/python3.11/dist-packages (from plotnine) (3.10.0)
Requirement already satisfied: pandas>=2.2.0 in /usr/local/lib/python3.11/dist-packages (from plotnine) (2.2.2)
Requirement already satisfied: mizani~=0.13.0 in /usr/local/lib/python3.11/dist-packages (from plotnine) (0.13.2)
Requirement already satisfied: numpy>=1.23.5 in /usr/local/lib/python3.11/dist-packages (from plotnine) (2.0.2)
Requirement already satisfied: scipy>=1.8.0 in /usr/local/lib/python3.11/dist-packages (from plotnine) (1.14.1)
Requirement already satisfied: statsmodels>=0.14.0 in /usr/local/lib/python3.11/dist-packages (from plotnine) (0.14.4)
Requirement already satisfied: pyogrio>=0.7.2 in /usr/local/lib/python3.11/dist-packages (from geopandas) (0.10.0)
Requirement already satisfied: packaging in /usr/local/lib/python3.11/dist-packages (from geopandas) (24.2)
Requirement already satisfied: pyproj>=3.3.0 in /usr/local/lib/python3.11/dist-packages (from geopandas) (3.7.1)
Requirement already satisfied: shapely>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from geopandas) (2.1.0)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (1.3.1)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (4.57.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (1.4.8)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (11.1.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (3.2.3)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.8.0->plotnine) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->plotnine) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->plotnine) (2025.2)
Requirement already satisfied: certifi in /usr/local/lib/python3.11/dist-packages (from pyogrio>=0.7.2->geopandas) (2025.1.31)
Requirement already satisfied: patsy>=0.5.6 in /usr/local/lib/python3.11/dist-packages (from statsmodels>=0.14.0->plotnine) (1.0.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.7->matplotlib>=3.8.0->plotnine) (1.17.0)
Code
from plotnine import *
import geopandas as gpd
import matplotlib.pyplot as plt

Net Migration Rates by Country 🌎

Some countries, such as Saudi Arabia, attract high numbers of migrants, while others, such as Guyana, experience significant emigration. The map shows a snapshot of current movement patterns.

Code
# Read shapefile
world = gpd.read_file('/content/ne_110m_admin_0_countries.shp')

# Fix known country name mismatches
name_fix = {
    'United States of America': 'United States',
    'Russia': 'Russian Federation',
    'Dem. Rep. Congo': 'Congo, the Democratic Republic of the',
    'Republic of the Congo': 'Congo, Rep.',
    'Viet Nam': 'Vietnam',
    'Iran': 'Iran, Islamic Republic of',
    'Syria': 'Syrian Arabic Republic',
    'United Republic of Tanzania': 'Tanzania',
    'Republic of Korea': 'South Korea',
    'North Korea': "Korea, Democratic People's Republic of",
    'Laos': "Lao People's Democratic Republic",
    'Czechia': 'Czech Republic',
    'Palestine': 'State of Palestine',
    'Tanzania': 'Tanzania, United Republic of',
    'Falkland Is.': 'Falkland Islands (Malvinas)',
    'Bolivia': 'Bolivia, Plurinational State of',
    'Venezuela': 'Venezuela, Bolivarian Republic of',
    "Côte d'Ivoire": 'Ivory Coast',
    'Central African Rep.': 'Central African Republic',
    'Moldova': 'Moldova, Republic of',
    'Libya': 'Libyan Arab Jamahiriya',
    'Bosnia and Herz.': 'Bosnia and Herzegovina',
    'North Macedonia': 'Macedonia, the former Yugoslav Republic of',
    'S. Sudan': 'South Sudan',
}
world['country_fixed'] = world['NAME'].replace(name_fix)

# Prepare migration data
avg_migration = migration.groupby('country', as_index=False)['obs_value'].mean()

# Merge using fixed names
map_df = world.merge(avg_migration, how='left', left_on='country_fixed', right_on='country')

# Plot the map
map_df.plot(column='obs_value', cmap='RdYlGn', legend=True, figsize=(15, 8), edgecolor='black', vmin=-10, vmax=10)
plt.title('Average Net Migration Rate by Country')
plt.axis('off')
plt.show()

Global Net Migration Over Time ⏳

Over the past few decades, global migration has shifted significantly. Notice the peaks and dips - what historical events might have driven these trends? Wars, economic crises, and policy changes all shape global migration flows!

Some of the most noticeable shifts in migration patterns occurred during the following periods:

  • 1960s, as more and more colonies gained independence
  • 1991, the collapse of Soviet Union
  • 2000s, due to the expansion of the European Union
Code
global_migration = migration.groupby('time_period', as_index=False)['obs_value'].mean()

ggplot(global_migration, aes('time_period', 'obs_value')) + \
    geom_line(color='#0077b6') + \
    labs(title='Global Average Net Migration Over Time',
         x='Year', y='Average Net Migration Rate') + \
    theme_minimal()

Migration Rate by Continent 🧭

Migration isn’t just about countries - it’s about entire regions. This bar chart shows which continents see more people arriving versus leaving. Europe and Asia generally have higher positive migration rates, while Africa and the Americas often see negative migration rates. Currently, Oceania has the lowest net migration rate in the world. These patterns reflect economic opportunities, political stability, and demographic trends.

Code
continent_map = {
    # Africa
    'Algeria': 'Africa', 'Angola': 'Africa', 'Benin': 'Africa', 'Botswana': 'Africa',
    'Burkina Faso': 'Africa', 'Burundi': 'Africa', 'Cabo Verde': 'Africa', 'Cameroon': 'Africa',
    'Central African Republic': 'Africa', 'Chad': 'Africa', 'Comoros': 'Africa', 'Congo, Rep.': 'Africa',
    'Ivory Coast': 'Africa', 'Congo, the Democratic Republic of the': 'Africa', 'Djibouti': 'Africa',
    'Egypt': 'Africa', 'Equatorial Guinea': 'Africa', 'Eritrea': 'Africa', 'Eswatini': 'Africa',
    'Ethiopia': 'Africa', 'Gabon': 'Africa', 'Gambia': 'Africa', 'Ghana': 'Africa',
    'Guinea': 'Africa', 'Guinea-Bissau': 'Africa', 'Kenya': 'Africa', 'Lesotho': 'Africa',
    'Liberia': 'Africa', 'Libyan Arab Jamahiriya': 'Africa', 'Madagascar': 'Africa', 'Malawi': 'Africa',
    'Mali': 'Africa', 'Mauritania': 'Africa', 'Mauritius': 'Africa', 'Morocco': 'Africa',
    'Mozambique': 'Africa', 'Namibia': 'Africa', 'Niger': 'Africa', 'Nigeria': 'Africa',
    'Rwanda': 'Africa', 'Sao Tome and Principe': 'Africa', 'Senegal': 'Africa', 'Seychelles': 'Africa',
    'Sierra Leone': 'Africa', 'Somalia': 'Africa', 'South Africa': 'Africa', 'South Sudan': 'Africa',
    'Sudan': 'Africa', 'Togo': 'Africa', 'Tunisia': 'Africa', 'Uganda': 'Africa',
    'Tanzania, United Republic of': 'Africa', 'Zambia': 'Africa', 'Zimbabwe': 'Africa',

    # Asia
    'Afghanistan': 'Asia', 'Armenia': 'Asia', 'Azerbaijan': 'Asia', 'Bahrain': 'Asia',
    'Bangladesh': 'Asia', 'Bhutan': 'Asia', 'Brunei Darussalam': 'Asia', 'Cambodia': 'Asia',
    'China': 'Asia', 'Cyprus': 'Asia', "Korea, Democratic People's Republic of": 'Asia',
    'Georgia': 'Asia', 'India': 'Asia', 'Indonesia': 'Asia', 'Iran, Islamic Republic of': 'Asia', 'Iraq': 'Asia',
    'Israel': 'Asia', 'Japan': 'Asia', 'Jordan': 'Asia', 'Kazakhstan': 'Asia', 'Kuwait': 'Asia',
    'Kyrgyzstan': 'Asia', "Lao People's Democratic Republic": 'Asia', 'Lebanon': 'Asia',
    'Malaysia': 'Asia', 'Maldives': 'Asia', 'Mongolia': 'Asia', 'Myanmar': 'Asia',
    'Nepal': 'Asia', 'Oman': 'Asia', 'Pakistan': 'Asia', 'State of Palestine': 'Asia', 'Philippines': 'Asia',
    'Qatar': 'Asia', 'South Korea': 'Asia', 'Saudi Arabia': 'Asia', 'Singapore': 'Asia',
    'Sri Lanka': 'Asia', 'Syrian Arab Republic': 'Asia', 'Tajikistan': 'Asia', 'Thailand': 'Asia',
    'Timor-Leste': 'Asia', 'Turkey': 'Asia', 'Turkmenistan': 'Asia', 'United Arab Emirates': 'Asia',
    'Uzbekistan': 'Asia', 'Vietnam': 'Asia', 'Yemen': 'Asia',

    # Europe
    'Albania': 'Europe', 'Andorra': 'Europe', 'Austria': 'Europe', 'Belarus': 'Europe',
    'Belgium': 'Europe', 'Bosnia and Herzegovina': 'Europe', 'Bulgaria': 'Europe', 'Croatia': 'Europe',
    'Czech Republic': 'Europe', 'Denmark': 'Europe', 'Estonia': 'Europe', 'Finland': 'Europe',
    'France': 'Europe', 'Germany': 'Europe', 'Greece': 'Europe', 'Hungary': 'Europe',
    'Iceland': 'Europe', 'Ireland': 'Europe', 'Italy': 'Europe', 'Latvia': 'Europe',
    'Lithuania': 'Europe', 'Luxembourg': 'Europe', 'Malta': 'Europe', 'Monaco': 'Europe',
    'Montenegro': 'Europe', 'Netherlands': 'Europe', 'Macedonia, the former Yugoslav Republic of': 'Europe',
    'Norway': 'Europe', 'Poland': 'Europe', 'Portugal': 'Europe', 'Moldova, Republic of': 'Europe',
    'Romania': 'Europe', 'San Marino': 'Europe', 'Serbia': 'Europe', 'Slovakia': 'Europe',
    'Slovenia': 'Europe', 'Spain': 'Europe', 'Sweden': 'Europe', 'Switzerland': 'Europe',
    'Ukraine': 'Europe', 'United Kingdom': 'Europe', 'Vatican City': 'Europe', 'Russian Federation': 'Europe',

    # North America
    'Antigua and Barbuda': 'North America', 'Bahamas': 'North America', 'Barbados': 'North America',
    'Belize': 'North America', 'Canada': 'North America', 'Costa Rica': 'North America',
    'Cuba': 'North America', 'Dominica': 'North America', 'Dominican Republic': 'North America',
    'El Salvador': 'North America', 'Grenada': 'North America', 'Guatemala': 'North America',
    'Haiti': 'North America', 'Honduras': 'North America', 'Jamaica': 'North America',
    'Mexico': 'North America', 'Nicaragua': 'North America', 'Panama': 'North America',
    'Saint Kitts and Nevis': 'North America', 'Saint Lucia': 'North America',
    'Saint Vincent and the Grenadines': 'North America', 'Trinidad and Tobago': 'North America',
    'United States': 'North America',

    # South America
    'Argentina': 'South America', 'Bolivia, Plurinational State of': 'South America', 'Brazil': 'South America',
    'Chile': 'South America', 'Colombia': 'South America', 'Ecuador': 'South America',
    'Guyana': 'South America', 'Paraguay': 'South America', 'Peru': 'South America',
    'Suriname': 'South America', 'Uruguay': 'South America', 'Venezuela, Bolivarian Republic of': 'South America',

    # Oceania
    'Australia': 'Oceania', 'Fiji': 'Oceania', 'Kiribati': 'Oceania', 'Marshall Islands': 'Oceania',
    'Micronesia (Federated States of)': 'Oceania', 'Nauru': 'Oceania', 'New Zealand': 'Oceania',
    'Palau': 'Oceania', 'Papua New Guinea': 'Oceania', 'Samoa': 'Oceania',
    'Solomon Islands': 'Oceania', 'Tonga': 'Oceania', 'Tuvalu': 'Oceania', 'Vanuatu': 'Oceania'
}

# Use .get() with .apply() to set unmatched countries to "Other"
migration['continent'] = migration['country'].apply(lambda x: continent_map.get(x, 'Other'))
Code
from pandas.api.types import CategoricalDtype
from plotnine import *

# Step 1: Group and sort the data
avg_by_continent = migration.groupby('continent', as_index=False)['obs_value'].mean()
avg_by_continent = avg_by_continent.sort_values('obs_value', ascending=False)

# Step 2: Create a custom column for color (green for positive, red for negative)
avg_by_continent['bar_color'] = avg_by_continent['obs_value'].apply(lambda x: 'green' if x >= 0 else 'red')

# Step 3: Reorder 'continent' for descending bar order
continent_order = avg_by_continent['continent'].tolist()
cat_type = CategoricalDtype(categories=continent_order, ordered=True)
avg_by_continent['continent'] = avg_by_continent['continent'].astype(cat_type)

# Step 4: Plot with color applied
ggplot(avg_by_continent, aes(x='continent', y='obs_value', fill='bar_color')) + \
    geom_bar(stat='identity') + \
    scale_fill_manual(values={'green': '#2ca02c', 'red': '#d62728'}) + \
    labs(title='Average Net Migration Rate by Continent',
         x='Continent', y='Net Migration Rate') + \
    theme_minimal() + \
    theme(legend_position='none')

Net Migration vs GDP per Capita 💰

This scatterplot suggests a general trend: richer countries tend to attract more migrants, while poorer countries often experience emigration.

Economic opportunity is a major driver of migration, but it isn’t the only factor. Some wealthy nations still see people leaving, while some poorer nations attract migrants due to other factors such as military stability or social policies.

This chart is interactive, hover over the points to explore different countries and regions.

Code
!pip install plotly
Requirement already satisfied: plotly in /usr/local/lib/python3.11/dist-packages (5.24.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.11/dist-packages (from plotly) (9.1.2)
Requirement already satisfied: packaging in /usr/local/lib/python3.11/dist-packages (from plotly) (24.2)
Code
import plotly.express as px
import plotly.graph_objects as go
import numpy as np

# Prepare the data again
avg_mig = migration.groupby('country', as_index=False)['obs_value'].mean()
avg_gdp = metadata.groupby('country', as_index=False)['GDP per capita (constant 2015 US$)'].mean()
merged = avg_mig.merge(avg_gdp, on='country')

# Add continent
merged['continent'] = merged['country'].map(continent_map)

# Drop rows with missing values in either column
merged = merged.dropna(subset=['obs_value', 'GDP per capita (constant 2015 US$)'])

# Get X and Y for regression
x = merged['GDP per capita (constant 2015 US$)']
y = merged['obs_value']

# Calculate linear regression line
slope, intercept = np.polyfit(x, y, 1)
regression_line = slope * x + intercept

# Create base scatterplot
fig = px.scatter(
    merged,
    x='GDP per capita (constant 2015 US$)',
    y='obs_value',
    color='continent',
    hover_name='country',
    title='Interactive: Net Migration Rate vs GDP per Capita (with Regression Line)',
    labels={
        'obs_value': 'Net Migration Rate',
        'GDP per capita (constant 2015 US$)': 'Average GDP per Capita (USD)'
    }
)

# Add regression line manually
fig.add_trace(
    go.Scatter(
        x=x,
        y=regression_line,
        mode='lines',
        name='Linear Regression',
        line=dict(color='black', dash='dash')
    )
)

# Show it
fig.show()

To Conclude…

Migration is shaped by a complex web of factors such as economic opportunity or political stability. This page highlights some key migration trends, and their connection to wealth and geography. While some migration is voluntary - seeking better jobs, education, or quality of life - other migration is forced, driven by war, climate change, and persecution.